Skip to content

feat(webapp): resolve which shard an environment mints run roots into - #4755

Merged
d-cs merged 19 commits into
mainfrom
feature/mint-shard-selection-tri-13428
Aug 24, 2026
Merged

feat(webapp): resolve which shard an environment mints run roots into#4755
d-cs merged 19 commits into
mainfrom
feature/mint-shard-selection-tri-13428

Conversation

@d-cs

@d-cs d-cs commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the shard-selection stage of run-id minting. resolveMintShard(env) returns which run-ops database an environment mints its new run roots into: the active shard list, then a fleet-wide override, then a per-environment or per-organization pin, then a rendezvous hash of the environment id.

That half is inert. Nothing calls resolveMintShard, no deployment has any of the new flags set, and an empty active list returns the current answer without reading anything.

The other half is not inert, and it is where review effort belongs. To stamp a grace window this needs a read-then-write under a lock, so it rewrites the global feature-flag write path that runOpsMintKind already depends on in production. See below.

Placement

Resolution reads the active list from a global flag, applies the grace window, and then picks:

  • a fleet-wide override if one is set, which is how a cutover completes without visiting each organization. new holds the whole fleet on the current id format.
  • otherwise a per-environment or per-organization pin. new holds one organization back while the rest move, which is how a canary works.
  • otherwise a rendezvous hash, so adding a shard moves only about 1/(N+1) of environments and removing one moves only its own.

Two hash details are load-bearing. Scores are 64-bit sha256(envId \0 key), because a 32-bit score collides at our environment count and an undetected tie would resolve by iteration order. The parsed key list is sorted, because otherwise two deployments listing the same shards in a different CSV order would place environments differently.

A pin or override naming a shard that has left the active list falls through to the hash and reports once. Honouring it would leak the drain the active list exists to perform, and throwing would fail triggers whenever a pinned shard drains.

Why the active list is a flag and not an environment variable

A deploy rolls for hours, so two pods hold two different environment values at the same time. A list held in the environment therefore splits the fleet for the length of the rollout, with new pods placing an environment on one shard and old pods on another. A grace window measured in seconds cannot cover that, and the same knob times the existing mint-kind flip so it cannot simply be lengthened. An environment variable also cannot record its own flip time, and an operator cannot know a rollout's end in advance.

So the list, its grace stamp and the override are global flags, written server-side against the control-plane clock under an advisory lock. This branch adds no environment variables.

The write path, which is live

Stamping generalises to any number of graced flag groups in one transaction under one lock. That has three consequences a reviewer should look at directly:

  • It closes a real bug. runOpsMintKind is an editable control on the global flags page, and that page previously wrote it with a bare upsert: no lock, no stamp. An operator flipping mint kind through the UI got an ungraced flip, so every pod crossed the cutover at a different moment. Verified against a running instance, before and after.
  • A graced group is all-or-nothing. Submitting its primary writes the group with a fresh stamp; omitting it deletes the primary and its stamp together, because a stamp left without its primary keeps being served and would mint into a shard just removed.
  • The advisory lock takes the previous id as well as the current one, in a fixed order, so writers on an older release still serialise during a rollout. The legacy id can be dropped one release after this ships.

This folds with #4751 rather than replacing it: its unlockLockedFlags rule decides what the sweep may delete, and the graced groups keep their stamp under the lock. Both sets of tests pass.

Notes for review

Determinism is a property of the pure core for fixed inputs. The wrapper supplies the clock, the same split effectiveMintKind already uses. A failed read of the list falls back to the current id format rather than guessing.

Six flags appear in the admin pages immediately. The two pins are per-organization, so they render read-only on the global page. The list, its stamp and the override are deployment-wide, so they render read-only in the organization dialog.

Nothing bounds the active list against shards that actually exist. That is safe while nothing mints, but the change that carries a shard key into an id must land after the shard descriptors bound the list, or bound it itself.

Adds the third stage of the run-id mint gate chain. `resolveMintShard(env)`
returns the shard key an environment mints new roots into: the active shard
list, then a per-env or per-org pin, then a rendezvous hash of the environment
id. With `RUN_OPS_MINT_SHARDS` unset or empty it returns "new", which is
today's behaviour, so this merges inert.

`computeRunIdMintKind` and `mintFlipGrace.ts` are untouched. The grace pattern
is cloned into `mintShardGrace.ts` rather than widened, so the existing
cuid/runOpsId flip grace keeps its behaviour.

Design notes:

- Pure core plus env-bound wrapper, mirroring `runOpsMintKind.server.ts`.
  Determinism is a property of `computeMintShard` for fixed deps; the wrapper
  supplies the clock, exactly as `effectiveMintKind` takes `nowMs`.
- Zero new queries on the trigger hot path. Both pins live in the org override
  blob that `mintRunFriendlyId` already holds.
- HRW scores `sha256(envId \0 key)` at 64 bits, over a sorted key list, with a
  lexicographic tie-break. A 32-bit score collides at our environment count,
  and without the sort two deployments listing the same keys in a different CSV
  order would place environments differently.
- `parseShardCsv` rejects anything outside [a-z0-9] and rejects the reserved
  keys at boot. `generateRunOpsIdV2` throws on an out-of-alphabet char, so an
  unvalidated key would become a throw on the mint path.
- A pin outside the active set falls through to the hash and reports once per
  environment per process. Honouring it would leak the drain the active list
  performs; throwing would fail customer triggers whenever a pinned shard
  drains. The loud-on-unknown-key rule governs reading an id, not writing one.
- "new" is a legal pin value, holding one org or environment on gen-1 while the
  rest of the fleet mints gen-2. Without it, a non-empty active set moves every
  environment at once.
- The active-set grace is stamped by `RUN_OPS_MINT_SHARDS_PREV` and
  `RUN_OPS_MINT_SHARDS_FLIPPED_AT`. A prev list with no timestamp is dropped; a
  timestamp with an empty prev list graces a first activation.

No changeset and no `.server-changes` note: nothing user-visible, and no caller
carries the returned key into an id yet.
@changeset-bot

changeset-bot Bot commented Aug 21, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 128d9aa

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The change adds gen-2 mint-shard feature flags, validation, pin and override handling, rendezvous-hash assignment, caching, and generation-1 fallbacks. It adds shared grace metadata for mint-kind and shard-set changes. Global flag writes now use transactional stamping, selective persistence, locked-flag validation, and derived-field removal. Admin confirmation lists include stored derived values. Tests cover validation, grace transitions, concurrency, routing, caching, and replacement behavior.

Merge Risk: 🟡 Moderate · up to 128d9

This PR adds shard pinning and routing controls, but malformed or inconsistently validated pin configuration can silently bypass an intended environment or organization pin and place new runs on a different shard. Merge should wait for these bounded routing-correctness risks to be fixed or explicitly accepted by the owner.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the design and risks, but it omits the required issue closure, checklist, Testing, Changelog, and Screenshots sections. Add the template sections, complete the checklist, document test commands and results, provide a changelog entry, and add screenshots or mark them not applicable.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: resolving the shard for environment run-root minting.
Docstring Coverage ✅ Passed Docstring check was indeterminate for this PR — some files could not be analyzed in time. Not blocking.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/mint-shard-selection-tri-13428

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@d-cs d-cs self-assigned this Aug 21, 2026
coderabbitai[bot]

This comment was marked as resolved.

d-cs added 2 commits August 24, 2026 09:42
… environment

A rolling deploy takes hours, so two pods run different values of
RUN_OPS_MINT_SHARDS at the same time. The grace window is sized in seconds,
so new pods left it long before old pods were gone: for the rest of the
rollout the two placed the same environment on different shards. That is the
divergence the grace exists to close.

The environment variable is now a ceiling that changes only by deploy. It
says which shard keys this deployment can mint into. The live list moves to
the control-plane database as runOpsMintShardSet, so every pod reads one
shared value whatever config generation it is running. Resolution intersects
the two, so a stored key this deployment cannot route is never minted into.

RUN_OPS_MINT_SHARDS_PREV and RUN_OPS_MINT_SHARDS_FLIPPED_AT are gone. An
environment variable cannot record its own flip time, and an operator cannot
know a rollout's end in advance. The stamp is now written server-side against
the control-plane clock, under an advisory lock, on a genuine change.

Stamping generalizes to N graced flag groups in one transaction under one
lock, covering the existing mint-kind trio and the new list. That closes a
hole on the global admin flags page, which wrote any catalog key with a bare
upsert: a graced key could be set with no stamp, or swept away by a save that
omitted it. applyGlobalMintKindFlip stays as a thin wrapper so its route and
its test keep working unchanged.

Operational rule this creates: every change to RUN_OPS_MINT_SHARDS must land
across the whole fleet before the flag selects a key it adds. Routing before
minting, which is how the shard topology is already gated.
Three areas of the change had no tests. The pure placement logic was well
covered; the production entry point and the safety claims were not.

resolveMintShard now takes its list reader as a dependency, the same way
computeRunIdMintKind takes its flag reader. That makes the cache, the TTL,
the ceiling short-circuit and the read fail-safe testable without a database
and without mocking. The fail-safe matters: a failed read returns gen-1
rather than guessing a list, because guessing would move every environment's
placement for the length of one blip.

The catalog tests pin the claim that a bad value is rejected at write. Until
now nothing checked it, so an unroutable shard key or a malformed pin blob
could have been stored and only failed later. The scope-lock tests pin each
key to the scope its resolver reads: pins are locked globally because they
are read from the org blob, and the list is locked per-org because it is
deployment-wide.

Still not covered, and needing a reviewer with Postgres and a browser: the
two admin write routes, and boot refusal on a malformed ceiling.
coderabbitai[bot]

This comment was marked as resolved.

d-cs added 2 commits August 24, 2026 09:58
Pins were per-organization and per-environment only, so completing a cutover
meant visiting every organization that still carried a canary pin. There was
no way to say "every environment mints here now".

runOpsMintShardOverride is a global flag that outranks every pin and the
hash. Setting it to "new" holds the whole fleet on the current id format,
which is the inverse lever for an emergency. It is honored only while the key
is in the active list, so it cannot mint into a drained or unroutable shard;
an override outside the list is reported and explicit pins still apply.

It is read in the same round-trip as the list it is bounded by, so it costs
no extra query on the trigger path, and it is locked per-organization because
an organization that could override the cutover lever would defeat it.

Also marks the ceiling seam: RUN_OPS_MINT_SHARDS is sourced in exactly one
place, and it should be deleted once shard descriptors are configured. The
descriptors already name every key this deployment can route, so keeping a
second hand-maintained list invites the two to drift.
RUN_OPS_MINT_SHARDS was added on this branch and never deployed, so there is
nothing to keep compatible. It duplicated information the shard descriptors
will own: a descriptor names every key this deployment can route, so a second
hand-maintained list only gives the two a way to disagree.

Its only job was to stop the list naming a key with no configured database.
Nothing here mints, so that cannot happen yet, and by the time it can the
descriptors exist and are the right source. Bounding the list belongs with
them.

The list flag alone is now the gate. Unset or empty means today's behaviour,
which is the state of every deployment that has not set it, so this stays
inert on merge. env.server.ts is untouched by this branch again.

Dependency this creates: the change that carries a shard key into an id must
not land before the descriptors bound the list, or it must bound the list
itself.
coderabbitai[bot]

This comment was marked as resolved.

Review found that this branch silently disabled the unset button for
runOpsMintKind on the global admin flags page. The graced keys were skipped
by the replace sweep so their stamp could not be bare-written, but that skip
covered the operator-supplied key as well as the server-computed ones. The
page omits a key to unset it, so the omission was read as "leave alone" and
the row survived. Before this branch the same gesture deleted it.

A graced group is now all-or-nothing. Submitting its primary writes the group
with a fresh stamp. Omitting the primary deletes the primary and its stamp
together, because a stamp left behind without its primary keeps being served:
an empty list beside a live prev list still resolves to the prev list for the
rest of the window, which would mint into a shard just removed.

Also from review:

- The whole save is one transaction again. The stamp, the upserts and the
  deletes could previously half-apply across two.
- The advisory lock takes the previous id as well as the current one, in a
  fixed order. A deploy rolls for hours, so renaming it left writers on the
  older release serializing against nothing. Drop the legacy id next release.
- A bad global override is reported once per value rather than once per
  environment. It applies to the whole fleet, so keying the report by
  environment turned one misconfiguration into a log line and a retained set
  entry per environment, on the trigger path. Both reporters are bounded now.
- The stamp keys render read-only. They were editable controls whose values
  were discarded on save.
- Groups name their primary and derived keys instead of relying on position.
- Corrected a claim in a comment: the cache TTL does not bound cross-process
  disagreement on its own, because the read goes to a replica. Stated why
  that is tolerable here specifically.
- The deprecated single-group entry point is gone; its test now covers the
  grouped one.
@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Observability map

As of 128d9aa.

Nothing in this pull request moves the report any more. The findings an earlier push reported are gone.

The score and findings here are report-only and never gate the merge. Separately, a required test suite keeps this tool's symbol and route lists in sync with the code they name, and can fail a pull request that renames or removes a symbol they reference, or that adds the first route with a segment they anticipate. Each failure names the list to edit. The rules and their reasons: internal-packages/observability-map/README.md.

d-cs added 9 commits August 24, 2026 10:27
…up fix

Running the Postgres suites surfaced two tests asserting opposite things
about the same gesture. One was written before groups became all-or-nothing
and expected the list to survive a save that omits it, which is the behaviour
that made unset a silent no-op. It is replaced with the property that is
actually correct: resubmitting the same list alongside another flag leaves
the list and its cutover clock alone.

Nothing in the implementation changed here. Only a test that encoded the old
bug did.
…ion helper

Both graced writes called client.$transaction directly. The repo rule is to
use the $transaction helper from db.server, which adds the OTEL span and logs
the infrastructure errors the raw client swallows. One of these writes stamps
a cutover window and the other deletes flags, so a transaction that silently
did not run is the case most worth seeing.

The helper resolves undefined instead of throwing when it swallows such an
error, so both call sites now treat that as a failure the caller sees.
Both global write routes carried their own copy of two answers: which flag
keys are graced, and which are server-computed. The JSON API named all four
derived keys in a destructure and both primaries in its branch condition. So
adding a graced group needed an edit in three files, and missing one would
either write an unstamped flip or accept a stamp from a request body.

Both answers now come from the group table. touchesGracedGroup and
withoutDerivedKeys are exported and used by the route, so a new group needs
no route change at all.

The global page's managed-cloud refusal moves into lockedFlagsInPayload, a
pure function, for the same reason: it encoded the locked-flag policy inline
where nothing could test it.

That is what closes the coverage gap. The routes previously held branch logic
reachable only through an authenticated request, so it went untested while
the function underneath it was well covered. The logic is now pure and tested
directly, including that only a graced PRIMARY selects the stamped path: a
body holding just a stamp must not reset a cutover clock.
…p helpers

The previous commit claimed both global write routes derive their graced-key
knowledge from the group table. Only one did. The JSON API imported the two
helpers and called neither, still naming all four derived keys in a
destructure and both primaries in its branch condition.

The edit that was supposed to replace that body silently matched nothing, and
nothing caught it: unused imports are not type errors, knip checks exports
rather than imports within a file, and the repo lint task is broken on its own
config so it never ran. The claim went into a commit message unverified.

The harm was real rather than cosmetic. A third graced group would have fallen
through to the direct write, which sets its stamp from the request body with
no lock, which is the failure the helpers exist to prevent.

Also from the same review:

- The advisory-lock comment had its reasoning inverted. The LEGACY id is the
  operative one, because an older release takes only that id and is what a
  rolling deploy has to serialize against. The new id adds nothing until every
  writer takes it.
- Two test comments claimed more than their tests did. The routing tests cover
  the helpers, not the routes, and cannot show that a route calls them; that is
  held by review. Said so.
- The concurrency test admitted every outcome, so it passed with the lock
  removed. It now asserts the stored pair is a coherent history: prev is what
  the other writer left, never the winner's own set.
- Merged a duplicate import of the same module.
…flags

Two defects this branch introduced.

The confirm dialog understated a deletion. Unsetting a graced primary clears
its two stamps, and this branch moved those stamps into the locked set, so
they left the page's editable keys and the change list stopped mentioning
them. Three rows were deleted and one was shown. Before this branch the
stamps were editable, so all three appeared.

The change list moves into buildFlagChangeList, which adds the cascade. Only
an unset cascades: a change re-stamps instead. A stamp that is not stored is
not listed. The key topology moves to the shared flag module, since the page
and the server both need it and a second copy would drift.

The save also wrote every submitted flag. Stamping needs read-then-write, so
this branch replaced a batch transaction with an interactive one, where each
upsert is its own round trip against the interactive timeout. It now reads
the submitted keys once and writes only the values that differ, so a typical
save costs two round trips rather than one per flag.
The protected-list case asserted only that the primary survives, so the
group-level property was unpinned: protection is keyed off the primary, and
the stamps must be kept with it. Third case on this branch of a test claiming
more than it asserted.
CI's oxlint flags an unbounded Prisma `in:` filter: the list length becomes
the bind-parameter count, so each distinct length is a separate prepared
statement. Both lists here are compile-time constants, but boundedIn is what
the neighbouring call sites use and it needs no suppression comment. It pads
to the next power of two by repeating the last key, which an IN over a unique
column does not notice.
…lection-tri-13428

# Conflicts:
#	apps/webapp/app/routes/admin.feature-flags.tsx
#	apps/webapp/app/v3/featureFlags.server.ts
The cascade disclosure never fired. buildFlagChangeList looked for the stamps
in initialValues, but the page builds initialValues by filtering locked keys
out, and the stamps are locked. So the confirm dialog still showed one removal
while three rows were deleted.

The unit test passed because it was handed an initialValues containing the
stamps, which the caller cannot produce. The cascade now reads storedValues,
the unfiltered set the loader returned, and the tests use the caller's real
shape. Verified against the running app: the dialog lists all three rows with
their values.
@d-cs
d-cs marked this pull request as ready for review August 24, 2026 12:17
d-cs added 2 commits August 24, 2026 13:19
…wrapper

The placement test reached env.server through its import of the .server
module, which the webapp guidance forbids: env.server parses the whole
environment schema at import, so the test either fails without a complete
environment or passes on ambient values.

The file already claimed the core was pure. It now is: mintShardAssignment.ts
holds the placement decision and the injected-deps resolver and imports no
env, no clock and no database. runOpsMintShard.server.ts keeps only the
env-bound wrapper, its caches and its reporters. The test moves next to the
pure module and no longer pulls env.server into its chain.

knip now sees the wrapper as an unused file rather than an unused export,
since nothing imports it until a shard key reaches an id. Ignored by path,
with a note to drop the entry with that change.
@pkg-pr-new

pkg-pr-new Bot commented Aug 24, 2026

Copy link
Copy Markdown

Open in StackBlitz

@trigger.dev/build

npm i https://pkg.pr.new/@trigger.dev/build@d8fb794

trigger.dev

npm i https://pkg.pr.new/trigger.dev@d8fb794

@trigger.dev/core

npm i https://pkg.pr.new/@trigger.dev/core@d8fb794

@trigger.dev/python

npm i https://pkg.pr.new/@trigger.dev/python@d8fb794

@trigger.dev/react-hooks

npm i https://pkg.pr.new/@trigger.dev/react-hooks@d8fb794

@trigger.dev/redis-worker

npm i https://pkg.pr.new/@trigger.dev/redis-worker@d8fb794

@trigger.dev/rsc

npm i https://pkg.pr.new/@trigger.dev/rsc@d8fb794

@trigger.dev/schema-to-json

npm i https://pkg.pr.new/@trigger.dev/schema-to-json@d8fb794

@trigger.dev/sdk

npm i https://pkg.pr.new/@trigger.dev/sdk@d8fb794

commit: d8fb794

devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
apps/webapp/app/v3/runOpsMigration/mintShardGrace.ts (1)

24-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tie the local flag-key literals to the catalog.

SET_KEY, SET_PREV_KEY, and SET_FLIPPED_AT_KEY repeat the catalog key names as string literals. runOpsMintShard.server.ts Lines 15-20 selects the same rows through FEATURE_FLAG.runOpsMintShardSet and friends. If a catalog key is renamed, this module keeps the old literal, readStoredCsv returns [], and every environment silently mints gen-1 with no error.

The module comment states the goal is to avoid importing the catalog. A compile-time assertion keeps that property and still fails the rename.

♻️ Proposed guard

Add to apps/webapp/test/runOpsMintShardFlags.test.ts:

it("keeps the pure module's flag keys equal to the catalog keys", () => {
  expect(FEATURE_FLAG.runOpsMintShardSet).toBe("runOpsMintShardSet");
  expect(FEATURE_FLAG.runOpsMintShardSetPrev).toBe("runOpsMintShardSetPrev");
  expect(FEATURE_FLAG.runOpsMintShardSetFlippedAt).toBe("runOpsMintShardSetFlippedAt");
});
apps/webapp/app/v3/featureFlags.server.ts (1)

189-192: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Bind each stamp function in the group table, not by a fallback ternary.

Line 191 selects stampMintKindFlip for runOpsMintKind and stampMintShardSetFlip for every other group. A third graced group would silently receive stampMintShardSetFlip. That function keys off its own runOpsMintShardSet constants, so it would ignore the new group's primary and stamp shard-set keys instead. The failure is silent: the new group flips with no grace window.

The comment on Lines 203-204 states that the group table prevents an unstamped flip. That holds for touchesGracedGroup but not for this binding.

Declare the stamp function per primary key and fail when a group has none.

♻️ Proposed change
+const STAMPERS: Record<string, typeof stampMintKindFlip> = {
+  [FEATURE_FLAG.runOpsMintKind]: stampMintKindFlip,
+  [FEATURE_FLAG.runOpsMintShardSet]: stampMintShardSetFlip,
+};
+
 const GRACED_GLOBAL_GROUPS = GRACED_FLAG_GROUPS.map((group) => {
-  ...group,
-  stamp: group.primary === FEATURE_FLAG.runOpsMintKind ? stampMintKindFlip : stampMintShardSetFlip,
-}));
+  const stamp = STAMPERS[group.primary];
+  if (!stamp) {
+    throw new Error(`graced flag group "${group.primary}" has no stamp function`);
+  }
+  return { ...group, stamp };
+});

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d2db509b-bdca-43c4-8ff3-9a795ac7ba94

📥 Commits

Reviewing files that changed from the base of the PR and between d645752 and d9a62ac.

📒 Files selected for processing (16)
  • apps/webapp/app/components/admin/flagChangeList.ts
  • apps/webapp/app/routes/admin.api.v1.feature-flags.ts
  • apps/webapp/app/routes/admin.feature-flags.tsx
  • apps/webapp/app/v3/featureFlags.server.ts
  • apps/webapp/app/v3/featureFlags.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.test.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardGrace.test.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardGrace.ts
  • apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.ts
  • apps/webapp/test/globalFlagChangeList.test.ts
  • apps/webapp/test/globalFlagWriteRouting.test.ts
  • apps/webapp/test/runOpsMintGlobalFlipLock.test.ts
  • apps/webapp/test/runOpsMintShardFlags.test.ts
  • apps/webapp/test/runOpsMintShardSetFlip.test.ts
  • knip.json
🚧 Files skipped from review as they are similar to previous changes (10)
  • apps/webapp/test/globalFlagWriteRouting.test.ts
  • apps/webapp/test/globalFlagChangeList.test.ts
  • apps/webapp/test/runOpsMintGlobalFlipLock.test.ts
  • apps/webapp/test/runOpsMintShardSetFlip.test.ts
  • knip.json
  • apps/webapp/app/v3/runOpsMigration/mintShardGrace.test.ts
  • apps/webapp/app/v3/featureFlags.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.test.ts
  • apps/webapp/app/routes/admin.api.v1.feature-flags.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 3 remain after this review.

📜 Review details
🧰 Additional context used
📓 Path-based instructions (12)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

**/*.{ts,tsx}: Prefer static imports over dynamic imports. Only use dynamic import() when:

  • Circular dependencies cannot be resolved otherwise
  • Code splitting is genuinely needed for performance
  • The module must be loaded conditionally at runtime

Files:

  • apps/webapp/test/runOpsMintShardFlags.test.ts
  • apps/webapp/app/components/admin/flagChangeList.ts
  • apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardGrace.ts
  • apps/webapp/app/v3/featureFlags.server.ts
  • apps/webapp/app/routes/admin.feature-flags.tsx
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use zod for validation in packages/core and apps/webapp

Files:

  • apps/webapp/test/runOpsMintShardFlags.test.ts
  • apps/webapp/app/components/admin/flagChangeList.ts
  • apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardGrace.ts
  • apps/webapp/app/v3/featureFlags.server.ts
  • apps/webapp/app/routes/admin.feature-flags.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Files:

  • apps/webapp/test/runOpsMintShardFlags.test.ts
  • apps/webapp/app/components/admin/flagChangeList.ts
  • apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardGrace.ts
  • apps/webapp/app/v3/featureFlags.server.ts
  • apps/webapp/app/routes/admin.feature-flags.tsx
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use vitest for all tests in the Trigger.dev repository

**/*.{test,spec}.{ts,tsx}: We use vitest exclusively. Never mock anything - use testcontainers instead.
Test files go next to source files (e.g., MyService.ts -> MyService.test.ts).

Files:

  • apps/webapp/test/runOpsMintShardFlags.test.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

**/*.ts: When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs
Do not use high-cardinality attributes in OTEL metrics such as UUIDs/IDs (envId, userId, runId, projectId, organizationId), unbounded integers (itemCount, batchSize, retryCount), timestamps (createdAt, startTime), or free-form strings (errorMessage, taskName, queueName)
When exporting OTEL metrics via OTLP to Prometheus, be aware that the exporter automatically adds unit suffixes to metric names (e.g., 'my_duration_ms' becomes 'my_duration_ms_milliseconds', 'my_counter' becomes 'my_counter_total'). Account for these transformations when writing Grafana dashboards or Prometheus queries

Files:

  • apps/webapp/test/runOpsMintShardFlags.test.ts
  • apps/webapp/app/components/admin/flagChangeList.ts
  • apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardGrace.ts
  • apps/webapp/app/v3/featureFlags.server.ts
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

apps/webapp/**/*.{ts,tsx}: Access environment variables through the env export of env.server.ts instead of directly accessing process.env
Use subpath exports from @trigger.dev/core package instead of importing from the root @trigger.dev/core path

Files:

  • apps/webapp/test/runOpsMintShardFlags.test.ts
  • apps/webapp/app/components/admin/flagChangeList.ts
  • apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardGrace.ts
  • apps/webapp/app/v3/featureFlags.server.ts
  • apps/webapp/app/routes/admin.feature-flags.tsx
apps/webapp/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Do not import env.server.ts directly or indirectly into test files; instead pass environment-dependent values through options/parameters to make code testable

Files:

  • apps/webapp/test/runOpsMintShardFlags.test.ts
apps/webapp/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)

Test files must not import app/env.server.ts; pass configuration as options instead.

Files:

  • apps/webapp/test/runOpsMintShardFlags.test.ts
**/*

📄 CodeRabbit inference engine (AGENTS.md)

**/*: Add crumbs as you write code — not just when debugging. Mark lines with
// @Crumbs or wrap blocks in `// `#region` `@crumbs. They stay on the
branch throughout development and are stripped by agentcrumbs strip
before merge.
Do not invent new namespaces — pick from this table or ask first.
Do not flag // @Crumbs markers, `#region `@crumbs blocks, or agentcrumbs
imports in reviews. These are temporary debug instrumentation stripped
before merge. Data logged in crumbs (IDs, names, values) never reaches
production.

Files:

  • apps/webapp/test/runOpsMintShardFlags.test.ts
  • apps/webapp/app/components/admin/flagChangeList.ts
  • apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardGrace.ts
  • apps/webapp/app/v3/featureFlags.server.ts
  • apps/webapp/app/routes/admin.feature-flags.tsx
apps/webapp/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)

apps/webapp/app/**/*.{ts,tsx}: For dashboard changes, visually verify the running Remix app with Chrome DevTools MCP, using snapshots, screenshots, interaction, and console-message checks as appropriate.
Use useCallback and useMemo only for context provider values, expensive derived data used as a dependency, or stable references required by dependency arrays; do not wrap ordinary event handlers or trivial computations.
Use named constants for sentinel or placeholder values instead of scattering raw string literals across comparisons.

Files:

  • apps/webapp/app/components/admin/flagChangeList.ts
  • apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardGrace.ts
  • apps/webapp/app/v3/featureFlags.server.ts
  • apps/webapp/app/routes/admin.feature-flags.tsx
apps/webapp/app/**/*.ts

📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)

apps/webapp/app/**/*.ts: Never use request.signal to detect client disconnects. Use getRequestAbortSignal() from app/services/httpAsyncStorage.server.ts, which is wired to Express response close events.
Access environment variables through the env export from app/env.server.ts; never use process.env directly.
Always use Prisma findFirst instead of findUnique.
Always use the $transaction helper from ~/db.server, never call prisma.$transaction or $replica.$transaction directly. Pass isolation levels as strings, use Serializable for correctness-critical read-then-write invariants, and guard possibly undefined helper results when a definite value is required.

Files:

  • apps/webapp/app/components/admin/flagChangeList.ts
  • apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardGrace.ts
  • apps/webapp/app/v3/featureFlags.server.ts
apps/webapp/app/v3/**/*.ts

📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)

New code must target Run Engine V2 through the singleton in app/v3/runEngine.server.ts; do not reintroduce V1 execution paths. V1 branches may only reject or finalize gracefully with a clean 4xx.

Files:

  • apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardGrace.ts
  • apps/webapp/app/v3/featureFlags.server.ts
🧠 Learnings (2)
📚 Learning: 2026-06-04T18:16:35.386Z
Learnt from: nicktrn
Repo: triggerdotdev/trigger.dev PR: 3836
File: apps/supervisor/src/backpressure/backpressureMonitor.ts:3-5
Timestamp: 2026-06-04T18:16:35.386Z
Learning: When reviewing TypeScript in this repo, apply the rule “prefer type aliases over interfaces” only to data/object shapes and union/intersection type modeling. If an interface is being used as a behavioral contract for collaborators to implement (e.g., method-shape interfaces that define required behavior, such as `BackpressureLogger` / `BackpressureSignalSource` in `apps/supervisor/src/backpressure/backpressureMonitor.ts`), keep it as an `interface` and do not flag it as a type-alias-vs-interface violation.

Applied to files:

  • apps/webapp/app/components/admin/flagChangeList.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardGrace.ts
📚 Learning: 2026-06-23T13:04:21.413Z
Learnt from: carderne
Repo: triggerdotdev/trigger.dev PR: 4023
File: apps/webapp/app/services/upsertBranch.server.ts:14-18
Timestamp: 2026-06-23T13:04:21.413Z
Learning: In TypeScript, it’s valid to `import { type X }` and then use `typeof X` in a type-only position, e.g. `type Alias = z.infer<typeof X>`. The `type` modifier suppresses the runtime import, but the type checker still has the full exported type so `z.infer<typeof X>` can resolve correctly. In code reviews, don’t flag this as a TypeScript compile error as long as `typeof X` is used in a type context (e.g., with `z.infer`, `type` aliases, generics), not as a runtime value.

Applied to files:

  • apps/webapp/app/v3/featureFlags.server.ts
🪛 ast-grep (0.45.1)
apps/webapp/app/v3/featureFlags.server.ts

[error] 331-350: Recursive/iterative merge copies attacker-controllable keys from a source object into a target via a computed property assignment without rejecting dangerous keys, allowing prototype pollution. Skip or block "proto", "constructor", and "prototype" keys (e.g. if (key === "__proto__" || key === "constructor" || key === "prototype") continue;), use a null-prototype object (Object.create(null)), or use a safe merge utility instead.
Context: for (const key of params.catalogKeys) {
const group = gracedGroupFor(key);

  if (group) {
    if (requestedFlags[group.primary] !== undefined) {
      if (stamped[key] !== undefined) {
        toWrite[key] = stamped[key];
      }
    } else if (!isProtected(group.primary)) {
      keysToDelete.push(key);
    }
    continue;
  }

  if (key in requestedFlags) {
    toWrite[key] = requestedFlags[key];
  } else if (!isProtected(key)) {
    keysToDelete.push(key);
  }
}

Note: [CWE-1321] Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution').

(prototype-pollution-recursive-merge-typescript)

🔇 Additional comments (8)
apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.ts (2)

1-1: The shard-set read still uses $replica.

A previous review asked for the primary client here, and the thread is marked as addressed. Line 1 and Line 27 still use $replica. applyGlobalGracedFlips writes the shard-set trio to the primary, so replica lag adds to RUN_OPS_MINT_FLAG_CACHE_TTL_MS. The window in which two pods serve different shard sets becomes TTL + replica lag, which the grace window does not account for.

The read runs once per process per TTL, so the primary can absorb it.

🔒 Proposed fix
-import { $replica, boundedIn } from "~/db.server";
+import { boundedIn, prisma } from "~/db.server";
@@
 async function readSetFlags(): Promise<Record<string, unknown>> {
-  const rows = await $replica.featureFlag.findMany({
+  const rows = await prisma.featureFlag.findMany({
     where: { key: { in: boundedIn(GLOBAL_SHARD_KEYS) } },
     select: { key: true, value: true },
   });

Also applies to: 26-36


41-67: LGTM!

Also applies to: 75-92

apps/webapp/app/v3/runOpsMigration/mintShardGrace.ts (1)

28-68: LGTM!

Also applies to: 74-99

apps/webapp/test/runOpsMintShardFlags.test.ts (1)

13-118: LGTM!

apps/webapp/app/v3/featureFlags.server.ts (2)

331-350: 📐 Maintainability & Code Quality

The prototype-pollution hint is a false positive.

Static analysis flags the computed assignments to toWrite. The loop iterates params.catalogKeys, which the caller derives from getAllFlagControlTypes() at apps/webapp/app/routes/admin.feature-flags.tsx Line 134. Those keys are catalog-defined, not request-controlled. No action is needed.

Source: Linters/SAST tools


245-248: LGTM!

Also applies to: 252-275, 279-296, 307-330, 352-382

apps/webapp/app/routes/admin.feature-flags.tsx (1)

116-121: LGTM!

Also applies to: 137-137, 402-402, 469-480, 492-498

apps/webapp/app/components/admin/flagChangeList.ts (1)

16-52: LGTM!

The route-action test mocks ~/db.server to inject its own Prisma client, and
that mock exported only prisma and boundedIn. replaceGlobalFeatureFlags now
takes the traced $transaction helper from the same module, so all five of the
existing cases failed with 'No "$transaction" export is defined'.

The stand-in keeps the real semantics under test, an interactive transaction
over the injected client, and drops only the tracing and the swallowed-error
handling, neither of which this test asserts.

Found by running a test file my earlier local runs had skipped. That file
arrived with #4751, and picking test files by hand is what hid it.
coderabbitai[bot]

This comment was marked as resolved.

…al txn helper

Two review findings.

The cache had a lost-update race. Two misses each issued a read, so a slower
read landing after a faster one wrote its older snapshot back into the cache
for a whole TTL. Refreshes are now single-flight: concurrent misses await one
read, and the in-flight handle is cleared on settle so a failure still lets
the next call retry. Three tests cover it with a deferred promise through the
injected reader, no mocking.

The admin action test's transaction stand-in was a reimplementation. It now
delegates to the same shared helper the production wrapper wraps, so the
transactional semantics, the nesting case and the retry behaviour are the real
ones. Only the wrapper's tracing span and infrastructure-error logging are
absent, and neither is asserted there.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/webapp/app/v3/runOpsMigration/mintShardAssignment.ts (1)

136-206: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add crumb markers for the changed cache-refresh paths.

Add // @Crumbs markers or `#region `@crumbs blocks while developing these paths. The repository guideline requires crumbs in all changed files.

  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.ts#L136-L206: Mark the single-flight refresh, cache write, and failure fallback paths.
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.test.ts#L322-L380: Mark the deferred-read, retry, and cache assertions.

As per coding guidelines: “Add crumbs as you write code.”

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f8a171b5-dffa-423d-b672-8333f9ed46f5

📥 Commits

Reviewing files that changed from the base of the PR and between b586544 and 128d9aa.

📒 Files selected for processing (3)
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.test.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.ts
  • apps/webapp/test/adminFeatureFlagsRouteAction.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 4 remain after this review.

📜 Review details
⏰ Context from checks skipped due to timeout. (35)
  • GitHub Check: report
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (23, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (17, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (21, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (20, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (18, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (24, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (14, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (22, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (12, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (16, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (8, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (19, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (15, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (11, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (10, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (9, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (13, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (1, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (3, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (6, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (2, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (5, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (7, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (4, 24)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: fk-cascade-guard / fk-cascade-guard
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (1, 2)
  • GitHub Check: runops-guard / runops-guard
  • GitHub Check: obsmap / 🧪 Unit Tests: Observability Map
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (2, 2)
  • GitHub Check: code-quality / code-quality
  • GitHub Check: audit
  • GitHub Check: audit
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (12)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

**/*.{ts,tsx}: Prefer static imports over dynamic imports. Only use dynamic import() when:

  • Circular dependencies cannot be resolved otherwise
  • Code splitting is genuinely needed for performance
  • The module must be loaded conditionally at runtime

Files:

  • apps/webapp/test/adminFeatureFlagsRouteAction.test.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.test.ts
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use zod for validation in packages/core and apps/webapp

Files:

  • apps/webapp/test/adminFeatureFlagsRouteAction.test.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.test.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Files:

  • apps/webapp/test/adminFeatureFlagsRouteAction.test.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.test.ts
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use vitest for all tests in the Trigger.dev repository

**/*.{test,spec}.{ts,tsx}: We use vitest exclusively. Never mock anything - use testcontainers instead.
Test files go next to source files (e.g., MyService.ts -> MyService.test.ts).

Files:

  • apps/webapp/test/adminFeatureFlagsRouteAction.test.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.test.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

**/*.ts: When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs
Do not use high-cardinality attributes in OTEL metrics such as UUIDs/IDs (envId, userId, runId, projectId, organizationId), unbounded integers (itemCount, batchSize, retryCount), timestamps (createdAt, startTime), or free-form strings (errorMessage, taskName, queueName)
When exporting OTEL metrics via OTLP to Prometheus, be aware that the exporter automatically adds unit suffixes to metric names (e.g., 'my_duration_ms' becomes 'my_duration_ms_milliseconds', 'my_counter' becomes 'my_counter_total'). Account for these transformations when writing Grafana dashboards or Prometheus queries

Files:

  • apps/webapp/test/adminFeatureFlagsRouteAction.test.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.test.ts
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

apps/webapp/**/*.{ts,tsx}: Access environment variables through the env export of env.server.ts instead of directly accessing process.env
Use subpath exports from @trigger.dev/core package instead of importing from the root @trigger.dev/core path

Files:

  • apps/webapp/test/adminFeatureFlagsRouteAction.test.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.test.ts
apps/webapp/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Do not import env.server.ts directly or indirectly into test files; instead pass environment-dependent values through options/parameters to make code testable

Files:

  • apps/webapp/test/adminFeatureFlagsRouteAction.test.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.test.ts
apps/webapp/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)

Test files must not import app/env.server.ts; pass configuration as options instead.

Files:

  • apps/webapp/test/adminFeatureFlagsRouteAction.test.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.test.ts
**/*

📄 CodeRabbit inference engine (AGENTS.md)

**/*: Add crumbs as you write code — not just when debugging. Mark lines with
// @Crumbs or wrap blocks in `// `#region` `@crumbs. They stay on the
branch throughout development and are stripped by agentcrumbs strip
before merge.
Do not invent new namespaces — pick from this table or ask first.
Do not flag // @Crumbs markers, `#region `@crumbs blocks, or agentcrumbs
imports in reviews. These are temporary debug instrumentation stripped
before merge. Data logged in crumbs (IDs, names, values) never reaches
production.

Files:

  • apps/webapp/test/adminFeatureFlagsRouteAction.test.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.test.ts
apps/webapp/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)

apps/webapp/app/**/*.{ts,tsx}: For dashboard changes, visually verify the running Remix app with Chrome DevTools MCP, using snapshots, screenshots, interaction, and console-message checks as appropriate.
Use useCallback and useMemo only for context provider values, expensive derived data used as a dependency, or stable references required by dependency arrays; do not wrap ordinary event handlers or trivial computations.
Use named constants for sentinel or placeholder values instead of scattering raw string literals across comparisons.

Files:

  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.test.ts
apps/webapp/app/**/*.ts

📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)

apps/webapp/app/**/*.ts: Never use request.signal to detect client disconnects. Use getRequestAbortSignal() from app/services/httpAsyncStorage.server.ts, which is wired to Express response close events.
Access environment variables through the env export from app/env.server.ts; never use process.env directly.
Always use Prisma findFirst instead of findUnique.
Always use the $transaction helper from ~/db.server, never call prisma.$transaction or $replica.$transaction directly. Pass isolation levels as strings, use Serializable for correctness-critical read-then-write invariants, and guard possibly undefined helper results when a definite value is required.

Files:

  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.test.ts
apps/webapp/app/v3/**/*.ts

📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)

New code must target Run Engine V2 through the singleton in app/v3/runEngine.server.ts; do not reintroduce V1 execution paths. V1 branches may only reject or finalize gracefully with a clean 4xx.

Files:

  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.ts
  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.test.ts
🧠 Learnings (2)
📚 Learning: 2026-06-04T18:16:35.386Z
Learnt from: nicktrn
Repo: triggerdotdev/trigger.dev PR: 3836
File: apps/supervisor/src/backpressure/backpressureMonitor.ts:3-5
Timestamp: 2026-06-04T18:16:35.386Z
Learning: When reviewing TypeScript in this repo, apply the rule “prefer type aliases over interfaces” only to data/object shapes and union/intersection type modeling. If an interface is being used as a behavioral contract for collaborators to implement (e.g., method-shape interfaces that define required behavior, such as `BackpressureLogger` / `BackpressureSignalSource` in `apps/supervisor/src/backpressure/backpressureMonitor.ts`), keep it as an `interface` and do not flag it as a type-alias-vs-interface violation.

Applied to files:

  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.ts
📚 Learning: 2026-08-24T12:38:01.585Z
Learnt from: d-cs
Repo: triggerdotdev/trigger.dev PR: 4755
File: apps/webapp/app/v3/runOpsMigration/mintShardAssignment.test.ts:1-9
Timestamp: 2026-08-24T12:38:01.585Z
Learning: Keep mint-shard assignment logic in the pure `mintShardAssignment.ts` module so it remains independent of `env.server.ts`, and test that logic in `mintShardAssignment.test.ts` without importing environment-bound modules. Keep runtime and environment integration in the `runOpsMintShard.server.ts` wrapper; do not reintroduce module-level boot warnings tied to the removed `RUN_OPS_MINT_SHARDS` setting.

Applied to files:

  • apps/webapp/app/v3/runOpsMigration/mintShardAssignment.test.ts
🔇 Additional comments (2)
apps/webapp/test/adminFeatureFlagsRouteAction.test.ts (2)

25-42: Keep this covered by the existing test-mocking finding.

This module substitute still mocks ~/db.server. The repository test rule forbids mocks in test files. This concern was already raised on a prior revision.

As per coding guidelines, **/*.{test,spec}.{ts,tsx} tests must not mock anything and must use testcontainers.

Source: Coding guidelines


5-5: LGTM!

@d-cs
d-cs merged commit f98e303 into main Aug 24, 2026
59 checks passed
@d-cs
d-cs deleted the feature/mint-shard-selection-tri-13428 branch August 24, 2026 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants